package networkcontroller;

import java.io.IOException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;

import networkcontroller.packet.AlivePacket;
import networkcontroller.packet.AskPacket;
import networkcontroller.packet.GetPacket;
import networkcontroller.packet.Packet;
import networkcontroller.packet.ResultPacket;
import networkcontroller.packet.SearchPacket;
import networkcontroller.packet.SegmentPacket;

import commands.controller.CC_NC_FreeConnections;
import commands.controller.CC_NC_GetSegment;
import commands.controller.CC_NC_PartOfFile;
import commands.controller.CC_NC_PartTimeout;
import commands.controller.CC_NC_SearchQuery;
import commands.nc.NCAliveInternal;
import commands.nc.NCErrorInternal;
import commands.nc.NCGet;
import commands.nc.NCGetInternal;
import commands.nc.NCResult;
import commands.nc.NCSearch;
import commands.nc.NCSearchInternal;
import commands.nc.NCSegment;
import commands.nc.NCSegmentInternal;
import commands.nc.NCSetMaxActiveConnectionsNumber;
import commands.nc.NetworkControllerCommand;

import controller.Controller;
import dataTypes.File;

/**
 * class which controls all network stuff
 * 
 * @author maciek
 * 
 */
public class NetworkController implements Runnable
{
	/**
	 * input queue of network controller
	 */
	private final LinkedBlockingQueue<NetworkControllerCommand>	inputQueue				= new LinkedBlockingQueue<NetworkControllerCommand>();
	/**
	 * reference to controller
	 */
	private final Controller									ctrl;
	/**
	 * local address of host
	 */
	private String												localAddr				= "127.0.0.1";
	/**
	 * map which handles incoming searches and its results
	 */
	private Map<Integer, NCSearchInternal>						incomingSearchMap		= new HashMap<Integer, NCSearchInternal>();
	/**
	 * map which handles incoming gets and its segments
	 */
	private Set<NCGetInternal>									incomingGetSet			= new HashSet<NCGetInternal>();
	/**
	 * map which handles outgoing gets and its segments
	 */
	private Map<TCPConnection, NCGet>							outgoingGetMap			= new HashMap<TCPConnection, NCGet>();
	/**
	 * current id of incoming search, incremented after each incoming search
	 */
	private int													currentSearchIncomingId	= 0;
	/**
	 * number of possible outgoing get connections in one time
	 */
	private int													downloadSlots			= 15;

	/**
	 * constructor which sets reference to controller and finds out local
	 * address
	 */
	public NetworkController(Controller ctrl)
	{
		// hack to find local address correctly
		try
		{
			Enumeration<NetworkInterface> netInterfaces = NetworkInterface.getNetworkInterfaces();
			while (netInterfaces.hasMoreElements())
			{
				NetworkInterface ni = (NetworkInterface) netInterfaces.nextElement();
				Enumeration<InetAddress> ips = ni.getInetAddresses();
				while (ips.hasMoreElements())
				{
					InetAddress ip = (InetAddress) ips.nextElement();
					String hostAddr = ip.getHostAddress();
					if (!ip.isLoopbackAddress() && hostAddr.indexOf(":") == -1)
					{
						localAddr = ip.getHostAddress();
					}
				}
			}
			System.out.println("NC: localaddr:" + localAddr);
		} catch (IOException e)
		{
			e.printStackTrace();
		}
		this.ctrl = ctrl;
	}

	public void run()
	{
		new MulticastListener(ctrl, this);
		new TCPListener(ctrl, this);
		NetworkControllerCommand cmd;
		while (true)
		{
			try
			{
				cmd = inputQueue.take();
				System.out.println("NC: run(): got new cmd:" + cmd + "command type:" + cmd.getCommandType());
				switch (cmd.getCommandType())
				{
				case GET:
					NCGet get = (NCGet) cmd;
					Packet packet = new GetPacket(get.getFile().getHash(), get.getFile().getSize(), get.getPartNumber());
					TCPConnection tcpConnection = new TCPConnection(get.getHost().getHostName(), NetworkConfiguration.getInstance().getPort(), ctrl,
							this, packet);
					outgoingGetMap.put(tcpConnection, get);
					break;
				case GET_INTERNAL:
					NCGetInternal getInternal = (NCGetInternal) cmd;
					incomingGetSet.add(getInternal);
					ctrl.writeMsg(new CC_NC_GetSegment(getInternal.getSum(), getInternal.getSize(), getInternal.getSegment()));
					// currentGetIncomingId++;
					break;
				case ALIVE_INTERNAL:
					NCAliveInternal aliveCmd = (NCAliveInternal) cmd;
					Packet alivePacket = new AlivePacket(localAddr, NetworkConfiguration.getInstance().getPort());
					new TCPConnection(aliveCmd.getHost(), aliveCmd.getPort(), ctrl, this, alivePacket);
					break;
				case ASK:
					new AskPacket(localAddr, NetworkConfiguration.getInstance().getPort());
					break;
				case SEARCH:
					if (cmd instanceof NCSearchInternal)
					{
						System.out.println("NC search internal");
						NCSearchInternal search = (NCSearchInternal) cmd;
						System.out.println("internal search adding from:" + search.getHost() + ":" + search.getPort());
						incomingSearchMap.put(currentSearchIncomingId, search);
						System.out.println(search.getMinSize() + " " + search.getMaxSize() + " " + search.getSum());
						ctrl.writeMsg(new CC_NC_SearchQuery(search.getSearchName(), search.getMinSize(), search.getMaxSize(), search.getSum(),
								currentSearchIncomingId));
						currentSearchIncomingId++;
					} else
					{
						NCSearch search = (NCSearch) cmd;
						new SearchPacket(false, localAddr, NetworkConfiguration.getInstance().getPort(), search.getId(), search.getSearchName(),
								search.getMinSize(), search.getMaxSize(), /* null */search.getSum());
					}
					break;
				case RESULT:
					NCResult result = (NCResult) cmd;
					NCSearchInternal search = incomingSearchMap.remove(result.getId());
					if (search != null)
					{
						Map<File, Float> map = result.getFileMap();
						if (map.size() != 0)
						{
							System.out.println("resultsize:" + map.size());
							Iterable<File> resultList = map.keySet();
							Packet resultPacket = new ResultPacket(localAddr, NetworkConfiguration.getInstance().getPort(), search.getId(),
									resultList);
							if (search.getTcpConnection() == null)
							{
								System.out.println("tcpconn null");
								new TCPConnection(search.getHost(), search.getPort(), ctrl, this, resultPacket);
							} else
							{
								TCPConnection conn = search.getTcpConnection();
								conn.writeMsg(resultPacket);
							}
						}
					} else
					{
						System.err.println("NC: no search to be replied");
					}
					break;
				case SEGMENT:
					NCSegment segment = (NCSegment) cmd;
					Iterator<NCGetInternal> iterator = incomingGetSet.iterator();
					boolean found = false;
					NCGetInternal message = null;
					while (iterator.hasNext())
					{
						message = iterator.next();
						if (message.getSegment() == segment.getPart_number())
							if (message.getSize() == segment.getSize())
								if (message.getSum().equals(segment.getHash()))
								{
									found = true;
									iterator.remove();
									break;
								}
					}
					if (found == true)
					{
						Packet segmentPacket = new SegmentPacket(segment.getData());
						TCPConnection connection = message.getTcpConnection();
						System.err.println("TCPconn:" + connection);
						connection.writeMsg(segmentPacket);
					} else
					{
						System.err.println("NC: notfound connection incominggetmap");
					}
					break;
				case SEGMENT_INTERNAL:
					NCSegmentInternal segmentInternal = (NCSegmentInternal) cmd;
					NCGet ncGet = outgoingGetMap.remove(segmentInternal.getTcpConnection());
					ctrl.writeMsg(new CC_NC_PartOfFile(ncGet.getFile(), ncGet.getPartNumber(), segmentInternal.getData()));
					break;
				case ERROR_INTERNAL:
					NCErrorInternal errorInternal = (NCErrorInternal) cmd;
					NCGet ncGetWithError = outgoingGetMap.remove(errorInternal.getConnection());
					if (ncGetWithError != null)
						ctrl.writeMsg(new CC_NC_PartTimeout(ncGetWithError.getFile(), ncGetWithError.getPartNumber(), ncGetWithError.getHost()));
					break;
				case GET_FREE_CONNECTIONS:
					ctrl.writeMsg(new CC_NC_FreeConnections(downloadSlots - outgoingGetMap.size()));
					break;
				case SET_MAX_ACTIVE_CONNECTIONS_NUMBER:
					NCSetMaxActiveConnectionsNumber maxConn = (NCSetMaxActiveConnectionsNumber) cmd;
					downloadSlots = maxConn.getMaxActiveConnectionNumber();
					break;
				}
			} catch (InterruptedException e)
			{
				e.printStackTrace();
			}
		}
	}

	/**
	 * puts commands to incoming command queue
	 * 
	 * @param cmd
	 *            incoming command
	 * @throws InterruptedException
	 */
	public void writeMsg(NetworkControllerCommand cmd) throws InterruptedException
	{
		synchronized (inputQueue)
		{
			if (inputQueue.remainingCapacity() == 0)
			{
				System.err.println("NC: writeMsg(): input queue is full - blocking call");
			}
			inputQueue.put(cmd);
		}
	}
}
